package org.codefilarete.stalactite.engine;

import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;

import org.codefilarete.reflection.MethodReferenceCapturer;
import org.codefilarete.stalactite.engine.runtime.BeanPersister;
import org.codefilarete.stalactite.query.builder.SQLBuilder;
import org.codefilarete.stalactite.query.model.Selectable;
import org.codefilarete.stalactite.sql.ConnectionProvider;
import org.codefilarete.stalactite.sql.result.Accumulator;
import org.codefilarete.stalactite.sql.result.Accumulators;
import org.codefilarete.stalactite.sql.result.BeanRelationFixer;
import org.codefilarete.stalactite.sql.result.MultipleColumnsReader;
import org.codefilarete.stalactite.sql.result.ResultSetRowAssembler;
import org.codefilarete.stalactite.sql.result.ResultSetRowTransformer;
import org.codefilarete.stalactite.sql.result.SingleColumnReader;
import org.codefilarete.stalactite.sql.result.WholeResultSetTransformer;
import org.codefilarete.stalactite.sql.result.WholeResultSetTransformer.AssemblyPolicy;
import org.codefilarete.stalactite.sql.statement.ReadOperation;
import org.codefilarete.stalactite.sql.statement.ReadOperationFactory;
import org.codefilarete.stalactite.sql.statement.StringParamedSQL;
import org.codefilarete.stalactite.sql.statement.binder.ColumnBinderRegistry;
import org.codefilarete.stalactite.sql.statement.binder.NullAwareParameterBinder;
import org.codefilarete.stalactite.sql.statement.binder.ParameterBinder;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.collection.Arrays;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.function.Converter;
import org.codefilarete.tool.function.SerializableTriFunction;
import org.danekja.java.util.function.serializable.SerializableBiConsumer;
import org.danekja.java.util.function.serializable.SerializableBiFunction;
import org.danekja.java.util.function.serializable.SerializableFunction;
import org.danekja.java.util.function.serializable.SerializableSupplier;

import static org.codefilarete.stalactite.sql.statement.binder.NullAwareParameterBinder.ALWAYS_SET_NULL_INSTANCE;
import static org.codefilarete.tool.bean.Objects.preventNull;

/**
 * A class aimed at querying the database and creating Java beans from it.
 * Beans resulting of it are not expected to be used as entity and given to {@link BeanPersister}, noticibly for insert
 * or update, because one should ensure that fields read from its query must be the same as those done by {@link BeanPersister}, otherwise it may
 * result in column value erasure.
 * 
 * @author Guillaume Mary
 */
public class QueryMapper<C> implements BeanKeyQueryMapper<C>, BeanPropertyQueryMapper<C> {
	
	/** Default method capturer. Shared to cache result of each lookup. */
	private static final MethodReferenceCapturer METHOD_REFERENCE_CAPTURER = new MethodReferenceCapturer();
	
	/** The method capturer (when column types are not given by {@link #map(String, SerializableBiConsumer)}) */
	private final MethodReferenceCapturer methodReferenceCapturer;
	
	/** Type of bean that will be built by {@link #execute(ConnectionProvider)} */
	private final Class<C> rootBeanType;
	
	/** The sql provider */
	private final SQLBuilder sqlBuilder;
	
	/** The registry of {@link ParameterBinder}s, for column reading as well as sql argument setting */
	private final ColumnBinderRegistry columnBinderRegistry;
	
	/** {@link ParameterBinder}s per ({@link java.sql.PreparedStatement}) parameter */
	private final Map<String, ParameterBinder<?>> sqlParameterBinders = new HashMap<>();
	
	/** SQL argument values (for where clause, or anywhere else) */
	private final Map<String, Object> sqlArguments = new HashMap<>();
	
	/** Delegate for {@link java.sql.ResultSet} transformation, will get all the mapping configuration */
	private WholeResultSetTransformer<C, ?> rootTransformer;
	
	private final QueryMapping<C, ?> mapping = new QueryMapping<>();
	
	private final ReadOperationFactory readOperationFactory;
	
	private final Map<Selectable<?>, String> aliases = new HashMap<>();
	
	/**
	 * Simple constructor
	 * 
	 * @param rootBeanType type of built bean
	 * @param sqlBuilder the sql to execute
	 * @param columnBinderRegistry a provider for SQL parameters and selected column
	 */
	public QueryMapper(Class<C> rootBeanType,
					   CharSequence sqlBuilder,
					   ColumnBinderRegistry columnBinderRegistry,
					   ReadOperationFactory readOperationFactory) {
		this(rootBeanType, sqlBuilder, columnBinderRegistry, METHOD_REFERENCE_CAPTURER, readOperationFactory);
	}
	
	/**
	 * Simple constructor
	 *
	 * @param rootBeanType type of built bean
	 * @param sqlBuilder the sql provider
	 * @param columnBinderRegistry a provider for SQL parameters and selected column
	 */
	public QueryMapper(Class<C> rootBeanType,
					   SQLBuilder sqlBuilder,
					   ColumnBinderRegistry columnBinderRegistry,
					   ReadOperationFactory readOperationFactory) {
		this(rootBeanType, sqlBuilder, columnBinderRegistry, METHOD_REFERENCE_CAPTURER, readOperationFactory);
	}
	
	/**
	 * Constructor to share {@link MethodReferenceCapturer} between instance of {@link QueryMapper}
	 * 
	 * @param rootBeanType type of built bean
	 * @param sqlBuilder the sql to execute
	 * @param columnBinderRegistry a provider for SQL parameters and selected column
	 * @param methodReferenceCapturer a method capturer (when column types are not given by {@link #map(String, SerializableBiConsumer)}),
	 * default is {@link #METHOD_REFERENCE_CAPTURER}
	 */
	public QueryMapper(Class<C> rootBeanType,
					   CharSequence sqlBuilder,
					   ColumnBinderRegistry columnBinderRegistry,
					   MethodReferenceCapturer methodReferenceCapturer,
					   ReadOperationFactory readOperationFactory) {
		this(rootBeanType, () -> sqlBuilder, columnBinderRegistry, methodReferenceCapturer, readOperationFactory);
	}
	
	/**
	 * Constructor to share {@link MethodReferenceCapturer} between instance of {@link QueryMapper}
	 *
	 * @param rootBeanType type of built bean
	 * @param sqlBuilder the sql provider
	 * @param columnBinderRegistry a provider for SQL parameters and selected column
	 * @param methodReferenceCapturer a method capturer (when column types are not given by {@link #map(String, SerializableBiConsumer)}),
	 * default is {@link #METHOD_REFERENCE_CAPTURER}
	 */
	public QueryMapper(Class<C> rootBeanType,
					   SQLBuilder sqlBuilder,
					   ColumnBinderRegistry columnBinderRegistry,
					   MethodReferenceCapturer methodReferenceCapturer,
					   ReadOperationFactory readOperationFactory) {
		this.rootBeanType = rootBeanType;
		this.sqlBuilder = sqlBuilder;
		this.columnBinderRegistry = columnBinderRegistry;
		this.methodReferenceCapturer = methodReferenceCapturer;
		this.readOperationFactory = readOperationFactory;
	}
	
	public void setColumnAliases(Map<Selectable<?>, String> aliases) {
		this.aliases.putAll(aliases);
	}
	
	@Override
	public QueryMapper<C> mapKey(SerializableSupplier<C> factory) {
		this.rootTransformer = new WholeResultSetTransformer<>(rootBeanType, factory);
		return this;
	}
	
	@Override
	public <I> QueryMapper<C> mapKey(SerializableFunction<I, C> factory, String columnName) {
		MethodReferenceCapturer methodReferenceCapturer = new MethodReferenceCapturer();
		// Looking for column type (necessary to know how to read ResultSet) by looking to first argument of given function
		// (which can be either a constructor or a method factory) 
		Executable executable = methodReferenceCapturer.findExecutable(factory);
		this.rootTransformer = buildSingleColumnKeyTransformer(new ColumnDefinition<>(columnName, (Class<I>) executable.getParameterTypes()[0]), factory);
		return this;
	}
	
	@Override
	public <I, J> QueryMapper<C> mapKey(SerializableBiFunction<I, J, C> factory, String column1, String column2) {
		MethodReferenceCapturer methodReferenceCapturer = new MethodReferenceCapturer();
		// Looking for column type (necessary to know how to read ResultSet) by looking to first argument of given function
		// (which can be either a constructor or a method factory) 
		Executable executable = methodReferenceCapturer.findExecutable(factory);
		SerializableFunction<Object[], C> constructorInvokation = args -> (C) factory.apply((I) args[0], (J) args[1]);
		this.rootTransformer = buildComposedKeyTransformer(Arrays.asSet(
				new ColumnDefinition<>(column1, executable.getParameterTypes()[0]),
				new ColumnDefinition<>(column2, executable.getParameterTypes()[1])),
				constructorInvokation);
		return this;
	}
	
	@Override
	public <I, J, K> QueryMapper<C> mapKey(SerializableTriFunction<I, J, K, C> factory, String column1, String column2, String column3) {
		MethodReferenceCapturer methodReferenceCapturer = new MethodReferenceCapturer();
		Executable executable = methodReferenceCapturer.findExecutable(factory);
		// Looking for column type (necessary to know how to read ResultSet) by looking to first argument of given function
		// (which can be either a constructor or a method factory) 
		SerializableFunction<Object[], C> constructorInvokation = args -> (C) factory.apply((I) args[0], (J) args[1], (K) args[2]);
		this.rootTransformer = buildComposedKeyTransformer(Arrays.asSet(
				new ColumnDefinition<>(column1, executable.getParameterTypes()[0]),
				new ColumnDefinition<>(column2, executable.getParameterTypes()[1]),
				new ColumnDefinition<>(column3, executable.getParameterTypes()[2])),
				constructorInvokation);
		return this;
	}
	
	/**
	 * Defines the key column and the way to create the bean : a constructor with the key as parameter.
	 *
	 * @param <I> the type of the column, which is also that of the factory argument
	 * @param factory the factory function that will instantiate new beans (with key as single argument)
	 * @param columnName the key column name
	 * @param columnType the type of the column, which is also that of the factory argument
	 * @return this
	 */
	@Override
	public <I> QueryMapper<C> mapKey(SerializableFunction<I, C> factory, String columnName, Class<I> columnType) {
		this.rootTransformer = buildSingleColumnKeyTransformer(new ColumnDefinition<>(columnName, columnType), factory);
		return this;
	}
	
	/**
	 * Defines key columns and the way to create the bean : a constructor with 2 arguments.
	 *
	 * @param <I> the type of the column, which is also that of the factory argument
	 * @param factory the factory function that will instantiate new beans (with key as single argument)
	 * @param column1Name the first key column name
	 * @param column1Type the type of the first column, which is also that of the factory first argument
	 * @param column2Name the second key column name
	 * @param column2Type the type of the second column, which is also that of the factory second argument
	 * @return this
	 */
	@Override
	public <I, J> QueryMapper<C> mapKey(SerializableBiFunction<I, J, C> factory, String column1Name, Class<I> column1Type,
										String column2Name, Class<J> column2Type) {
		SerializableFunction<Object[], C> constructorInvokation = args -> (C) factory.apply((I) args[0], (J) args[1]);
		this.rootTransformer = buildComposedKeyTransformer(Arrays.asSet(
				new ColumnDefinition<>(column1Name, column1Type),
				new ColumnDefinition<>(column2Name, column2Type)),
				constructorInvokation);
		return this;
	}
	
	/**
	 * Defines key columns and the way to create the bean : a constructor with 3 arguments.
	 *
	 * @param <I> the type of the column, which is also that of the factory argument
	 * @param factory the factory function that will instantiate new beans (with key as single argument)
	 * @param column1Name the first key column name
	 * @param column1Type the type of the first column, which is also that of the factory first argument
	 * @param column2Name the second key column name
	 * @param column2Type the type of the second column, which is also that of the factory second argument
	 * @param column3Name the third key column name
	 * @param column3Type the type of the third column, which is also that of the factory third argument
	 * @return this
	 */
	@Override
	public <I, J, K> QueryMapper<C> mapKey(SerializableTriFunction<I, J, K, C> factory, String column1Name, Class<I> column1Type,
										   String column2Name, Class<J> column2Type,
										   String column3Name, Class<K> column3Type) {
		SerializableFunction<Object[], C> constructorInvokation = args -> (C) factory.apply((I) args[0], (J) args[1], (K) args[2]);
		this.rootTransformer = buildComposedKeyTransformer(Arrays.asSet(
				new ColumnDefinition<>(column1Name, column1Type),
				new ColumnDefinition<>(column2Name, column2Type),
				new ColumnDefinition<>(column3Name, column3Type)),
				constructorInvokation);
		return this;
	}
	
	/**
	 * Same as {@link #mapKey(SerializableFunction, String, Class)} but with {@link Selectable} signature
	 *
	 * @param <I> type of the key
	 * @param factory the factory function that will instantiate new beans (with key as single argument)
	 * @param column the mapped column used as a key
	 * @return this
	 */
	@Override
	public <I> QueryMapper<C> mapKey(SerializableFunction<I, C> factory,
									 Selectable<I> column) {
		this.rootTransformer = buildSingleColumnKeyTransformer(new ColumnWrapper<>(column), factory);
		return this;
	}
	
	/**
	 * Same as {@link #mapKey(SerializableFunction, Selectable)} with a 2-args constructor
	 *
	 * @param <I> type of the key
	 * @param factory the factory function that will instantiate new beans (with key as single argument)
	 * @param column1 the first column of the key
	 * @param column2 the second column of the key
	 * @return this
	 */
	@Override
	public <I, J> QueryMapper<C> mapKey(SerializableBiFunction<I, J, C> factory,
										Selectable<I> column1,
										Selectable<J> column2
	) {
		SerializableFunction<Object[], C> constructorInvokation = args -> (C) factory.apply((I) args[0], (J) args[1]);
		this.rootTransformer = buildComposedKeyTransformer(Arrays.asSet(
				new ColumnWrapper<>(column1),
				new ColumnWrapper<>(column2)),
				constructorInvokation);
		return this;
	}
	
	/**
	 * Same as {@link #mapKey(SerializableFunction, Selectable)} with a 3-args constructor
	 *
	 * @param <I> type of the key
	 * @param factory the factory function that will instantiate new beans (with key as single argument)
	 * @param column1 the first column of the key
	 * @param column2 the second column of the key
	 * @param column3 the third column of the key
	 * @return this
	 */
	@Override
	public <I, J, K> QueryMapper<C> mapKey(SerializableTriFunction<I, J, K, C> factory,
										   Selectable<I> column1,
										   Selectable<J> column2,
										   Selectable<K> column3
	) {
		SerializableFunction<Object[], C> constructorInvokation = args -> (C) factory.apply((I) args[0], (J) args[1], (K) args[2]);
		this.rootTransformer = buildComposedKeyTransformer(Arrays.asSet(
				new ColumnWrapper<>(column1),
				new ColumnWrapper<>(column2),
				new ColumnWrapper<>(column3)),
				constructorInvokation);
		return this;
	}
	
	@Override
	public <I> QueryMapper<I> mapKey(String columnName, Class<I> columnType) {
		this.rootTransformer = (WholeResultSetTransformer<C, ?>) buildSingleColumnKeyTransformer(new ColumnDefinition<>(columnName, columnType), SerializableFunction.identity());
		return (QueryMapper<I>) this;
	}
	
	/**
	 * Defines a mapping between a column of the query and a bean property through its setter
	 *
	 * @param columnName a column name
	 * @param setter the setter function
	 * @param columnType the type of the column, which is also that of the setter argument
	 * @param <I> the type of the column, which is also that of the setter argument
	 * @return this
	 */
	@Override
	public <I> QueryMapper<C> map(String columnName, BiConsumer<C, I> setter, Class<I> columnType) {
		map(new ColumnMapping<>(columnName, setter, columnType));
		return this;
	}
	
	/**
	 * Same as {@link #map(String, BiConsumer, Class)}.
	 * Differs by providing the possiblity to convert the value before setting it onto the bean.
	 *
	 * @param columnName a column name
	 * @param setter the setter function
	 * @param columnType the type of the column, which is also that of the setter argument
	 * @param converter a converter of the read value from ResultSet
	 * @param <I> the type of the setter argument, which is also that of the converter result
	 * @param <J> the type of the column, which is also that of the converter argument
	 * @return this
	 */
	@Override
	public <I, J> QueryMapper<C> map(String columnName, SerializableBiConsumer<C, J> setter, Class<I> columnType, Converter<I, J> converter) {
		return map(columnName, (c, i) -> setter.accept(c, converter.convert(i)), columnType);
	}
	
	/**
	 * Defines a mapping between a column of the query and a bean property through its setter.
	 * WARNING : Column type will be deduced from the setter type. To do it, some bytecode enhancement is required, therefore it's not the cleanest
	 * way to define the binding. Prefer {@link #map(String, BiConsumer, Class)}
	 *
	 * @param columnName a column name
	 * @param setter the setter function
	 * @param <I> the type of the column, which is also that of the setter argument
	 * @return this
	 */
	@Override
	public <I> QueryMapper<C> map(String columnName, SerializableBiConsumer<C, I> setter) {
		map(columnName, setter, giveColumnType(setter));
		return this;
	}
	
	/**
	 * Same as {@link #map(String, SerializableBiConsumer)}.
	 * Differs by providing the possibility to convert the value before setting it onto the bean.
	 *
	 * @param columnName a column name
	 * @param setter the setter function
	 * @param converter a converter of the read value from ResultSet
	 * @param <I> the type of the setter argument, which is also that of the converter result
	 * @return this
	 */
	@Override
	public <I, J> QueryMapper<C> map(String columnName, SerializableBiConsumer<C, J> setter, Converter<I, J> converter) {
		Method method = methodReferenceCapturer.findMethod(setter);
		Class<I> aClass = (Class<I>) method.getParameterTypes()[0];
		return map(columnName, (c, i) -> setter.accept(c, converter.convert(i)), aClass);
	}
	
	/**
	 * Same as {@link #map(String, BiConsumer, Class)} but with {@link Selectable} signature
	 *
	 * @param column the mapped column
	 * @param setter the setter function
	 * @param <I> the type of the column, which is also that of the setter argument
	 * @return this
	 */
	@Override
	public <I> QueryMapper<C> map(Selectable<I> column, BiConsumer<C, I> setter) {
		map(new ColumnMapping<>(column, setter));
		return this;
	}
	
	/**
	 * Same as {@link #map(Selectable, BiConsumer)}.
	 * Differs by providing the possibility to convert the value before setting it onto the bean.
	 *
	 * @param column the mapped column
	 * @param setter the setter function
	 * @param converter a converter of the read value from ResultSet
	 * @param <I> the type of the setter argument, which is also that of the converter result
	 * @param <J> the type of the column, which is also that of the converter argument
	 * @return this
	 */
	@Override
	public <I, J> QueryMapper<C> map(Selectable<I> column,
									 BiConsumer<C, J> setter,
									 Converter<I, J> converter) {
		return map(column, (c, i) -> setter.accept(c, converter.convert(i)));
	}
	
	private <I> void map(ColumnMapping<C, I> columnMapping) {
		mapping.add((ColumnMapping) columnMapping);
	}
	
	/** Overridden to adapt return type */
	@Override
	public QueryMapper<C> map(ResultSetRowAssembler<C> assembler) {
		return map(assembler, AssemblyPolicy.ON_EACH_ROW);
	}
	
	@Override
	public QueryMapper<C> map(ResultSetRowAssembler<C> assembler, AssemblyPolicy assemblyPolicy) {
		mapping.add(assembler, assemblyPolicy);
		return this;
	}
	
	@Override
	public <K, V> QueryMapper<C> map(BeanRelationFixer<C, V> combiner, ResultSetRowTransformer<V, K> relatedBeanCreator) {
		mapping.add((BeanRelationFixer) combiner, relatedBeanCreator);
		return this;
	}
	
	/**
	 * Executes the query onto the connection given by the {@link ConnectionProvider}. Transforms the result to a list of beans thanks to the
	 * definition given through {@link #mapKey(SerializableFunction, String, Class)}, {@link #map(String, BiConsumer, Class)}
	 * and {@link #map(String, SerializableBiConsumer)} methods.
	 *
	 * @param connectionProvider the object that will give the {@link java.sql.Connection}
	 * @return a {@link Set} filled by the instances built
	 */
	public Set<C> execute(ConnectionProvider connectionProvider) {
		return execute(connectionProvider, Accumulators.toSet());
	}
	
	public <R> R execute(ConnectionProvider connectionProvider, Accumulator<C, ?, R> accumulator) {
		WholeResultSetTransformer<C, ?> transformerToUse;
		if (rootTransformer == null) {
			// No key defined (none of mapKey(..) methods were invoked), therefore we'll use default (no-arg) constructor
			transformerToUse = new WholeResultSetTransformer<>(rootBeanType, () -> Reflections.newInstance(rootBeanType));
		} else {
			transformerToUse = rootTransformer;
		}
		this.mapping.applyTo((WholeResultSetTransformer) transformerToUse);
		StringParamedSQL parameterizedSQL = new StringParamedSQL(this.sqlBuilder.toSQL().toString(), sqlParameterBinders);
		parameterizedSQL.setValues(sqlArguments);
		try (ReadOperation<String> readOperation = readOperationFactory.createInstance(parameterizedSQL, connectionProvider)) {
			return transformerToUse.transformAll(readOperation.execute(), accumulator);
		}
	}
	
	private <I, O> WholeResultSetTransformer<O, I> buildSingleColumnKeyTransformer(Column<I> keyColumn, SerializableFunction<I, O> beanFactory) {
		return new WholeResultSetTransformer<>((Class<O>) rootBeanType, keyColumn.getName(), keyColumn.getBinder(), beanFactory);
	}
	
	private WholeResultSetTransformer<C, Object[]> buildComposedKeyTransformer(Set<Column> columns, SerializableFunction<Object[], C> beanFactory) {
		Map<Column<?>, String> computedAliases = new HashMap<>();
		Set<SingleColumnReader> columnReaders = Iterables.collect(columns, c -> {
			ParameterBinder reader = c.getBinder();
			String alias;
			if (c instanceof ColumnWrapper) {
				alias = preventNull(aliases.get(((ColumnWrapper<?>) c).getColumn()), c.getName());
			} else {
				alias = c.getName();
			}
			computedAliases.put(c, alias);
			return new SingleColumnReader<>(alias, reader);
		}, HashSet::new);
		MultipleColumnsReader<Object[]> multipleColumnsReader = new MultipleColumnsReader<>(columnReaders, resultSetRow -> {
			// we transform all columns value into a Object[]
			Object[] constructorArgs = new Object[columns.size()];
			int i = 0;
			for (Column column : columns) {
				constructorArgs[i++] = resultSetRow.get(computedAliases.get(column));
			}
			return constructorArgs;
		});
		
		ResultSetRowTransformer<C, Object[]> resultSetRowConverter = new ResultSetRowTransformer<>(
				rootBeanType,
				multipleColumnsReader,
				beanFactory);
		return new WholeResultSetTransformer<>(resultSetRowConverter);
	}
	
	private <I> Class<I> giveColumnType(SerializableBiConsumer<C, I> setter) {
		Method method = methodReferenceCapturer.findMethod(setter);
		// we could take the first parameter type, but with a particular syntax of setter it's insufficient, last element is better 
		return (Class<I>) Arrays.last(method.getParameterTypes());
	}
	
	/**
	 * Sets a value for a SQL parameter. Not to be used with Collection/Iterable value : see {@link #set(String, Iterable, Class)} dedicated method for it.
	 * No check for "already set" argument is done, so one can overwrite/redefine an existing value. This lets one reexecutes a
	 * {@link QueryMapper} with different parameters.
	 *
	 * @param paramName the name of the SQL parameter to be set
	 * @param value the value of the parameter, null is authorized but since type can't be know in this case {@link java.sql.PreparedStatement#setObject(int, Object)}
	 * 				will be used, so prefer {@link #set(String, Object, Class)} if your database driver doesn't support well setObject(..),
	 * 			    see	{@link NullAwareParameterBinder#ALWAYS_SET_NULL_INSTANCE}
	 * @return this
	 * @see #set(String, Iterable, Class)
	 */
	public QueryMapper<C> set(String paramName, Object value) {
		return set(paramName, value, value == null ? null : (Class) value.getClass());
	}
	
	/**
	 * Sets a value for a SQL parameter. Not for Collection/Iterable value : see {@link #set(String, Iterable, Class)} dedicated method for it.
	 * No check for "already set" argument is done, so one can overwrite/redefine an existing value. This lets one reexecutes a
	 * {@link QueryMapper} with different parameters.
	 *
	 * @param paramName the name of the SQL parameter to be set
	 * @param value the value of the parameter
	 * @param valueType the content type of the {@link Iterable}, more exactly will determine which {@link ParameterBinder} to be used
	 * @return this
	 * @see #set(String, Iterable, Class)
	 */
	public <O> QueryMapper<C> set(String paramName, O value, Class<? super O> valueType) {
		ParameterBinder<?> parameterBinder;
		if (value == null) {
			parameterBinder = ALWAYS_SET_NULL_INSTANCE;
		} else {
			if (value.getClass().isArray()) {
				valueType = (Class<? super O>) Iterables.first((Object[]) value);
			} else if (Iterable.class.isAssignableFrom(value.getClass())) {
				valueType = (Class<? super O>) Iterables.first((Iterable) value).getClass();
			}
			parameterBinder = columnBinderRegistry.getBinder(valueType);
		}
		this.sqlParameterBinders.put(paramName, parameterBinder);
		this.sqlArguments.put(paramName, value);
		return this;
	}
	
	/**
	 * Sets a value for a Collection/Iterable SQL argument. Must be distinguished from {@link #set(String, Object)} because real {@link Iterable}
	 * content type guessing can be difficult (or at least not accurate) and can lead to {@link Iterable} consumption.
	 * No check for "already set" argument is done, so one can overwrite/redefine an existing value. This lets one reexecutes a
	 * {@link QueryMapper} with different parameters.
	 *
	 * @param paramName the name of the SQL parameter to be set
	 * @param value the value of the parameter
	 * @param valueType the content type of the {@link Iterable}, more exactly will determine which {@link ParameterBinder} to be used
	 * @return this
	 */
	public <O> QueryMapper<C> set(String paramName, Iterable<O> value, Class<? super O> valueType) {
		this.sqlParameterBinders.put(paramName, value == null ? ALWAYS_SET_NULL_INSTANCE : columnBinderRegistry.getBinder(valueType));
		this.sqlArguments.put(paramName, value);
		return this;
	}
	
	/**
	 * An internal definition of a "column" : a selected column or a statement parameter
	 * @param <T> the value type of the "column"
	 */
	private interface Column<T> {
		
		String getName();
		
		ParameterBinder<T> getBinder();
	}
	
	private class ColumnDefinition<T> implements QueryMapper.Column<T> {
		
		private final String name;
		
		private final Class<T> valueType;
		
		private ColumnDefinition(String name, Class<T> valueType) {
			this.name = name;
			this.valueType = valueType;
		}
		
		public String getName() {
			return name;
		}
		
		public ParameterBinder<T> getBinder() {
			return columnBinderRegistry.getBinder(valueType);
		}
	}
	
	private class ColumnWrapper<T> implements QueryMapper.Column<T> {
		
		private final Selectable<T> column;
		
		private ColumnWrapper(Selectable<T> column) {
			this.column = column;
		}
		
		public String getName() {
			return column.getExpression();
		}
		
		public ParameterBinder<T> getBinder() {
			if (column instanceof org.codefilarete.stalactite.sql.ddl.structure.Column) {
				return columnBinderRegistry.getBinder((org.codefilarete.stalactite.sql.ddl.structure.Column<?, T>) column);
			} else {
				return columnBinderRegistry.getBinder(column.getJavaType());
			}
		}
		
		public Selectable<T> getColumn() {
			return column;
		}
	}
	
	/**
	 * An internal class defining the way to map a result column to a bean "property" (more precisely a setter or whatever which takes the value as input)
	 * 
	 * @param <T> the bean type that supports the setter
	 * @param <I> the column value type which is also the input type of the property setter
	 */
	private class ColumnMapping<T, I> {
		
		private final QueryMapper.Column<I> column;
		
		private final BiConsumer<T, I> setter;
		
		public ColumnMapping(String columnName, BiConsumer<T, I> setter, Class<I> columnType) {
			this.column = new ColumnDefinition<>(columnName, columnType);
			this.setter = setter;
		}
		
		public ColumnMapping(Selectable<I> column, BiConsumer<T, I> setter) {
			this.column = new ColumnWrapper<>(column);
			this.setter = setter;
		}
		
		public QueryMapper.Column<I> getColumn() {
			return column;
		}
		
		public BiConsumer<T, I> getSetter() {
			return setter;
		}
	}
	
	
	/**
	 * Stores configuration defined by user to let user uses {@link QueryMapper} methods in any order : without it mapKey(..) should be used first
	 * because we might have to apply mapping to {@link WholeResultSetTransformer} instance
	 * 
	 * @param <C> created instance type
	 * @param <I> identifier type
	 */
	private static class QueryMapping<C, I> {
		
		private final List<Mapping> mappings = new ArrayList<>(10);
		
		public void add(QueryMapper<C>.ColumnMapping<C, I> columnMapping) {
			mappings.add(new ColumnMapping(columnMapping));
		}
		
		public void add(ResultSetRowAssembler<C> assembler, AssemblyPolicy assemblyPolicy) {
			mappings.add(new AssemblerMapping(assembler, assemblyPolicy));
		}
		
		public <K, V> void add(BeanRelationFixer<K, V> combiner, ResultSetRowTransformer<V, K> relatedBeanCreator) {
			mappings.add(new RelationMapping(combiner, relatedBeanCreator));
		}
		
		/**
		 * Transfer this mapping to given instance
		 * @param target instance that will consume current mapping
		 */
		public void applyTo(WholeResultSetTransformer<C, I> target) {
			this.mappings.forEach(m -> {
				if (m instanceof ColumnMapping) {
					QueryMapper.ColumnMapping columnMapping = ((ColumnMapping) m).columnMapping;
					target.add(columnMapping.getColumn().getName(), columnMapping.getColumn().getBinder(), columnMapping.getSetter());
				} else if (m instanceof AssemblerMapping) {
					AssemblerMapping assemblerMapping = (AssemblerMapping) m;
					target.add(assemblerMapping.assembler, assemblerMapping.policy);
				} else if (m instanceof RelationMapping) {
					RelationMapping relationMapping = (RelationMapping) m;
					target.add((BeanRelationFixer<C, Object>) relationMapping.combiner::apply, relationMapping.relatedBeanCreator);
				}
			});
		}
		
		/**
		 * A marking interface for different kind of mapping configurations, made to allow them being stored in {@link java.util.Collection}.
		 * Only used by {@link QueryMapping}
		 */
		private interface Mapping {
			
		}
		
		/** Simple store for property-column mapping */
		private static class ColumnMapping implements Mapping {
			private final QueryMapper.ColumnMapping columnMapping;
			
			private ColumnMapping(QueryMapper.ColumnMapping columnMapping) {
				this.columnMapping = columnMapping;
			}
		}
		
		/** Simple store for assembly mapping */
		private static class AssemblerMapping implements Mapping {
			private final ResultSetRowAssembler assembler;
			private final AssemblyPolicy policy;
			
			private AssemblerMapping(ResultSetRowAssembler assembler, AssemblyPolicy policy) {
				this.assembler = assembler;
				this.policy = policy;
			}
		}
		
		/** Simple store for relation mapping */
		private static class RelationMapping implements Mapping {
			private final BeanRelationFixer combiner;
			private final ResultSetRowTransformer relatedBeanCreator;
			
			private RelationMapping(BeanRelationFixer combiner, ResultSetRowTransformer relatedBeanCreator) {
				this.combiner = combiner;
				this.relatedBeanCreator = relatedBeanCreator;
			}
		}
	}
}
